/*
 * Funambol is a mobile platform developed by Funambol, Inc. 
 * Copyright (C) 2003 - 2007 Funambol, Inc.
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission 
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE 
 * WARRANTY OF NON INFRINGEMENT  OF THIRD PARTY RIGHTS.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License 
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 * 
 * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite 
 * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
 * 
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 * 
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by Funambol" logo. If the display of the logo is not reasonably 
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by Funambol".
 */

package com.funambol.util;

import java.util.Calendar;
import java.util.Date;

/**
 * A utility class providing methods to convert date information contained in
 * <code>Date</code> objects into RFC2822 and UTC ('Zulu') strings, and to build
 * <code>Date</code> objects starting from string representations of dates in
 * RFC2822 and UTC format
 */
public class MailDateFormatter {

	/** Format date as: MM/DD */
	public static final int FORMAT_MONTH_DAY = 0;
	/** Format date as: MM/DD/YYYY */
	public static final int FORMAT_MONTH_DAY_YEAR = 1;
	/** Format date as: hh:mm */
	public static final int FORMAT_HOURS_MINUTES = 2;
	/** Format date as: hh:mm:ss */
	public static final int FORMAT_HOURS_MINUTES_SECONDS = 3;
	/** Format date as: DD/MM */
	public static final int FORMAT_DAY_MONTH = 4;
	/** Format date as: DD/MM/YYYY */
	public static final int FORMAT_DAY_MONTH_YEAR = 5;

	/** Device offset, as string */
	private static String deviceOffset = "+0000";
	/** Device offset, in millis */
	private static long millisDeviceOffset = 0;

	/** Names of the months */
	private static String[] monthNames = new String[] { "Jan", "Feb", "Mar",
			"Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

	/**
	 * Transforms data contained in a <code>Date</code> object (expressed in
	 * UTC) in a string formatted as per RFC2822 in local time (par. 3.3)
	 * 
	 * @return A string representing the date contained in the passed
	 *         <code>Date</code> object formatted as per RFC 2822 and in local
	 *         time
	 */
	public static String dateToRfc2822(final Date date) {

		final Calendar deviceTime = Calendar.getInstance();
		deviceTime.setTime(date);

		String dayweek = "";
		final int dayOfWeek = deviceTime.get(Calendar.DAY_OF_WEEK);
		switch (dayOfWeek) {
		case 1:
			dayweek = "Sun";
			break;
		case 2:
			dayweek = "Mon";
			break;
		case 3:
			dayweek = "Tue";
			break;
		case 4:
			dayweek = "Wed";
			break;
		case 5:
			dayweek = "Thu";
			break;
		case 6:
			dayweek = "Fri";
			break;
		case 7:
			dayweek = "Sat";
			break;
		}

		final int dayOfMonth = deviceTime.get(Calendar.DAY_OF_MONTH);

		final String monthInYear = MailDateFormatter.getMonthName(deviceTime
				.get(Calendar.MONTH));

		final int year = deviceTime.get(Calendar.YEAR);

		final int hourOfDay = deviceTime.get(Calendar.HOUR_OF_DAY);
		final int minutes = deviceTime.get(Calendar.MINUTE);
		final int seconds = deviceTime.get(Calendar.SECOND);

		final String rfc = dayweek + ", " + // Tue
				dayOfMonth + " " + // 7
				monthInYear + " " + // Nov
				year + " " + // 2006
				hourOfDay + ":" + minutes + ":" + seconds + " " + // 14:13:26
				MailDateFormatter.deviceOffset; // +0200
		return rfc;
	}

	/**
	 * Converts a <code>Date</code> object into a string in 'Zulu' format
	 * 
	 * @param d
	 *            A <code>Date</code> object to be converted into a string in
	 *            'Zulu' format
	 * @return A string representing the date contained in the passed
	 *         <code>Date</code> object in 'Zulu' format (e.g. yyyyMMDDThhmmssZ)
	 */
	public static String dateToUTC(final Date d) {
		final StringBuffer date = new StringBuffer();
		final Calendar cal = Calendar.getInstance();
		cal.setTime(d);

		date.append(cal.get(Calendar.YEAR));

		date.append(
				MailDateFormatter.printTwoDigits(cal.get(Calendar.MONTH) + 1))
				.append(
						MailDateFormatter
								.printTwoDigits(cal.get(Calendar.DATE)))
				.append("T");

		date
				.append(
						MailDateFormatter.printTwoDigits(cal
								.get(Calendar.HOUR_OF_DAY))).append(
						MailDateFormatter.printTwoDigits(cal
								.get(Calendar.MINUTE))).append(
						MailDateFormatter.printTwoDigits(cal
								.get(Calendar.SECOND))).append("Z");

		return date.toString();
	}

	/**
	 * A method that returns a string rapresenting a date.
	 * 
	 * @param date
	 *            the date
	 * 
	 * @param format
	 *            the format as one of FORMAT_MONTH_DAY, FORMAT_MONTH_DAY_YEAR,
	 *            FORMAT_HOURS_MINUTES, FORMAT_HOURS_MINUTES_SECONDS
	 *            FORMAT_DAY_MONTH FORMAT_DAY_MONTH_YEAR constants
	 * 
	 * @param separator
	 *            the separator to be used
	 */
	public static String getFormattedStringFromDate(final Date date,
			final int format, final String separator) {

		final Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		final StringBuffer ret = new StringBuffer();

		switch (format) {
		case FORMAT_HOURS_MINUTES:
			// if pm and hour == 0 we want to write 12, not 0
			if (cal.get(Calendar.AM_PM) == Calendar.PM
					&& cal.get(Calendar.HOUR) == 0)
				ret.append("12");
			else
				ret.append(cal.get(Calendar.HOUR));
			ret.append(separator).append(
					MailDateFormatter.printTwoDigits(cal.get(Calendar.MINUTE)))
					.append(MailDateFormatter.getAMPM(cal));
			break;

		case FORMAT_HOURS_MINUTES_SECONDS:
			// if pm and hour == 0 we want to write 12, not 0
			if (cal.get(Calendar.AM_PM) == Calendar.PM
					&& cal.get(Calendar.HOUR) == 0)
				ret.append("12");
			else
				ret.append(cal.get(Calendar.HOUR));
			ret.append(separator).append(
					MailDateFormatter.printTwoDigits(cal.get(Calendar.MINUTE)))
					.append(separator).append(cal.get(Calendar.SECOND)).append(
							MailDateFormatter.getAMPM(cal));
			break;

		case FORMAT_MONTH_DAY:
			ret.append(cal.get(Calendar.MONTH) + 1).append(separator).append(
					cal.get(Calendar.DAY_OF_MONTH));
			break;

		case FORMAT_DAY_MONTH:
			ret.append(cal.get(Calendar.DAY_OF_MONTH)).append(separator)
					.append(cal.get(Calendar.MONTH) + 1);
			break;

		case FORMAT_MONTH_DAY_YEAR:
			ret.append(cal.get(Calendar.MONTH) + 1).append(separator).append(
					cal.get(Calendar.DAY_OF_MONTH)).append(separator).append(
					cal.get(Calendar.YEAR));
			break;

		case FORMAT_DAY_MONTH_YEAR:
			ret.append(cal.get(Calendar.DAY_OF_MONTH)).append(separator)
					.append(cal.get(Calendar.MONTH) + 1).append(separator)
					.append(cal.get(Calendar.YEAR));
			break;

		default:
			Log.error("getFormattedStringFromDate: invalid format (" + format
					+ ")");
		}

		return ret.toString();
	}

	/**
	 * Returns a localized string representation of Date.
	 */
	public static String formatLocalTime(final Date d) {
		int dateFormat = MailDateFormatter.FORMAT_MONTH_DAY_YEAR;
		final int timeFormat = MailDateFormatter.FORMAT_HOURS_MINUTES;

		if (!System.getProperty("microedition.locale").equals("en"))
			dateFormat = MailDateFormatter.FORMAT_DAY_MONTH_YEAR;
		return MailDateFormatter.getFormattedStringFromDate(d, dateFormat, "/")
				+ " "
				+ MailDateFormatter.getFormattedStringFromDate(d, timeFormat,
						":");
	}

	/**
	 * Parses the string in RFC 2822 format and return a <code>Date</code>
	 * object.
	 * <p>
	 * Parse strings like: Thu, 03 May 2007 14:45:38 GMT Thu, 03 May 2007
	 * 14:45:38 GMT+0200 Thu, 1 Feb 2007 03:57:01 -0800 Fri, 04 May 2007
	 * 13:40:17 PDT
	 * 
	 * @param d
	 *            the date representation to parse
	 * @return a date, if valid, or null on error
	 * 
	 */
	public static Date parseRfc2822Date(String stringDate) {
		if (stringDate == null)
			return null;

		long hourOffset = 0;
		long minOffset = 0;
		final Calendar cal = Calendar.getInstance();

		try {

			Log.info("Date original: " + stringDate);

			// Just skip the weekday if present
			int start = stringDate.indexOf(',');
			// put start after ", "
			start = (start == -1) ? 0 : start + 2;

			stringDate = stringDate.substring(start).trim();
			start = 0;

			// Get day of month
			int end = stringDate.indexOf(' ', start);

			int day = 1;
			try {
				day = Integer.parseInt(stringDate.substring(start, end));
			} catch (final NumberFormatException ex) {
				// some phones (Nokia 6111) have a invalid date format,
				// something like Tue10 Jul... instead of Tue, 10
				// so we try to strip (again) the weekday
				day = Integer.parseInt(stringDate.substring(start + 3, end));
				Log.info("Nokia 6111 patch applied.");
			}

			cal.set(Calendar.DAY_OF_MONTH, day);

			// Get month
			start = end + 1;
			end = stringDate.indexOf(' ', start);
			cal.set(Calendar.MONTH, MailDateFormatter.getMonthNumber(stringDate
					.substring(start, end)));
			// Get year
			start = end + 1;
			end = stringDate.indexOf(' ', start);
			cal.set(Calendar.YEAR, Integer.parseInt(stringDate.substring(start,
					end)));
			// Get hour
			start = end + 1;
			end = stringDate.indexOf(':', start);
			cal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(stringDate
					.substring(start, end).trim()));
			// Get min
			start = end + 1;
			end = stringDate.indexOf(':', start);
			cal.set(Calendar.MINUTE, Integer.parseInt(stringDate.substring(
					start, end)));
			// Get sec
			start = end + 1;
			end = stringDate.indexOf(' ', start);
			cal.set(Calendar.SECOND, Integer.parseInt(stringDate.substring(
					start, end)));
			// Get OFFSET
			start = end + 1;
			end = stringDate.indexOf('\r', start);

			// Process Timezone, checking first for the actual RFC2822 format,
			// and then for nthe obsolete syntax.

			char sign = '+';
			String hourDiff = "0";
			String minDiff = "0";

			final String offset = stringDate.substring(start).trim();
			if (offset.startsWith("+") || offset.startsWith("-")) {
				if (offset.length() >= 5) {
					sign = offset.charAt(0);
					hourDiff = offset.substring(1, 3);
					minDiff = offset.substring(3, 5);
				} else if (offset.length() == 3) {
					sign = offset.charAt(0);
					hourDiff = offset.substring(1);
					minDiff = "00";
				}
				// Convert offset to int
				hourOffset = Long.parseLong(hourDiff);
				minOffset = Long.parseLong(minDiff);
				if (sign == '-')
					hourOffset = -hourOffset;
			} else if (offset.equals("EDT"))
				hourOffset = -4;
			else if (offset.equals("EST") || offset.equals("CDT"))
				hourOffset = -5;
			else if (offset.equals("CST") || offset.equals("MDT"))
				hourOffset = -6;
			else if (offset.equals("PDT") || offset.equals("MST"))
				hourOffset = -7;
			else if (offset.equals("PST"))
				hourOffset = -8;
			else if (offset.equals("GMT") || offset.equals("UT"))
				hourOffset = 0;
			else if (offset.substring(0, 3).equals("GMT")
					&& offset.length() > 3) {
				sign = offset.charAt(3);
				hourDiff = offset.substring(4, 6);
				minDiff = offset.substring(6, 8);
			}

			final long millisOffset = (hourOffset * 3600000)
					+ (minOffset * 60000);

			final Date gmtDate = cal.getTime();
			long millisDate = gmtDate.getTime();

			millisDate -= millisOffset;

			gmtDate.setTime(millisDate);
			return gmtDate;

		} catch (final Exception e) {
			Log.error("Exception in parseRfc2822Date: " + e.toString()
					+ " parsing " + stringDate);
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Convert the given date (GMT) into the local date. NOTE: changes the
	 * original date too! Should we change it to a void toLocalDate(Date) that
	 * changes the input date only?
	 */
	public static Date getDeviceLocalDate(final Date gmtDate) {
		if (null != gmtDate) {
			/*
			 * long dateInMillis = gmtDate.getTime(); Date deviceDate = new
			 * Date(); deviceDate.setTime(dateInMillis+millisDeviceOffset);
			 * return deviceDate;
			 */
			gmtDate.setTime(gmtDate.getTime()
					+ MailDateFormatter.millisDeviceOffset);
			return gmtDate;
		} else
			return null;
	}

	/**
	 * Gets a <code>Date</code> object from a string representing a date in
	 * 'Zulu' format (yyyyMMddTHHmmssZ)
	 * 
	 * @param utc
	 *            date in 'Zulu' format (yyyyMMddTHHmmssZ)
	 * @return A <code>Date</code> object obtained starting from a time in
	 *         milliseconds from the Epoch
	 */
	public static Date parseUTCDate(final String utc) {

		int day = 0;
		int month = 0;
		int year = 0;
		int hour = 0;
		int minute = 0;
		int second = 0;
		Calendar calendar = null;

		day = Integer.parseInt(utc.substring(6, 8));
		month = Integer.parseInt(utc.substring(4, 6));
		year = Integer.parseInt(utc.substring(0, 4));
		hour = Integer.parseInt(utc.substring(9, 11));
		minute = Integer.parseInt(utc.substring(11, 13));
		second = Integer.parseInt(utc.substring(13, 15));

		calendar = Calendar.getInstance();

		calendar.set(Calendar.DAY_OF_MONTH, day);
		calendar.set(Calendar.MONTH, month - 1);
		calendar.set(Calendar.YEAR, year);
		calendar.set(Calendar.HOUR_OF_DAY, hour);
		calendar.set(Calendar.MINUTE, minute);
		calendar.set(Calendar.SECOND, second);

		final Date date = calendar.getTime();
		final long dateInMillis = date.getTime();

		date.setTime(dateInMillis + MailDateFormatter.millisDeviceOffset);

		return date;
	}

	public static void setTimeZone(final String timeZone) {

		if (timeZone == null || timeZone.length() < 5)
			Log.error("setTimeZone: invalid timezone " + timeZone);

		try {
			MailDateFormatter.deviceOffset = timeZone;
			final String hstmz = MailDateFormatter.deviceOffset.substring(1, 3);
			final String mstmz = MailDateFormatter.deviceOffset.substring(3, 5);

			final long hhtmz = Long.parseLong(hstmz);
			final long mmtmz = Long.parseLong(mstmz);
			MailDateFormatter.millisDeviceOffset = (hhtmz * 3600000)
					+ (mmtmz * 60000);
			if (MailDateFormatter.deviceOffset.charAt(0) == '-')
				MailDateFormatter.millisDeviceOffset *= -1;

		} catch (final Exception e) {
			Log.error("setTimeZone: " + e.toString());
			e.printStackTrace();
		}
	}

	// ------------------------------------------------------------- Private
	// methods

	/**
	 * Get the number of the month, given the name.
	 */
	private static int getMonthNumber(final String name) {
		for (int i = 0, l = MailDateFormatter.monthNames.length; i < l; i++)
			if (MailDateFormatter.monthNames[i].equals(name))
				return i;
		return -1;
	}

	/**
	 * Get the name of the month, given the number.
	 */
	private static String getMonthName(final int number) {
		if (number > 0 && number < MailDateFormatter.monthNames.length)
			return MailDateFormatter.monthNames[number];
		else
			return null;
	}

	private static String getAMPM(final Calendar cal) {
		return (cal.get(Calendar.AM_PM) == Calendar.AM) ? "a" : "p";
	}

	/**
	 * Returns a string representation of number with at least 2 digits
	 */
	private static String printTwoDigits(final int number) {
		if (number > 9)
			return String.valueOf(number);
		else
			return "0" + number;
	}

}
